Praca domowa 3

Mateusz Krzyziński

Praca domowa nr 3 dotyczy metody wyjaśnień lokalnych przy użyciu metody Ceteris Paribus, która ocenia wpływ zmian wybranej zmiennej objaśniającej na zmiany predykcji modelu. Jest to więc metoda bardziej szczegółówa od poprzednich i w piramidzie eksploracji modelu znajduje się poziom niżej.

In [1]:
import pandas as pd
import numpy as np
import pickle
import dalex as dx
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.impute import SimpleImputer
import warnings
warnings.filterwarnings('ignore')

Dane

In [2]:
# Wczytanie i przygotowanie danych 
full_data = pd.read_csv("data/hotel_bookings.csv")
full_data["agent"] = full_data["agent"].astype(str)
treshold = 0.005 * len(full_data)
agents_to_change = full_data['agent'].value_counts()[full_data['agent'].value_counts() < treshold].index
full_data.loc[full_data["agent"].isin(agents_to_change), "agent"] = "other"

countries_to_change = full_data['country'].value_counts()[full_data['country'].value_counts() < treshold].index
full_data.loc[full_data["country"].isin(countries_to_change), "country"] = "other"


# Określenie cech uwzględnionych w modelu
num_features = ["lead_time", "arrival_date_week_number",
                "stays_in_weekend_nights", "stays_in_week_nights", 
                "adults", "previous_cancellations",
                "previous_bookings_not_canceled",
                "required_car_parking_spaces", "total_of_special_requests", 
                "adr", "booking_changes"]

cat_features = ["hotel", "market_segment", "country", 
                "reserved_room_type",
                "customer_type", "agent"]

features = num_features + cat_features

# Podział na zmienne wyjaśniające i target
X = full_data.drop(["is_canceled"], axis=1)[features]
y = full_data["is_canceled"]
In [3]:
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2, random_state=42)

Model

W tej pracy domowej wykorzystuję model, który koduje zmienne kateogryczne, wykorzystując one hot encoding. Dzięki temu możemy bardziej intuicyjnie (w postaci wykresów słupkowych) przeglądać wygenerowane profile Ceteris Paribus dla zmiennych o charakterze kategorycznym danej.

Sam model wczytuję z pickle'a, a odpowiedniego notebooka dotyczącego modelowania można znaleźć już na repozytorium.

In [4]:
rf_model = pickle.load(open("RF_model", 'rb'))
In [5]:
predict_train = rf_model.predict(X_train)
predict_proba_train = rf_model.predict_proba(X_train)

Wyjaśnienia

In [6]:
explainer = dx.Explainer(rf_model, X_train, y_train, label = "RF classifier with OH encoding")
Preparation of a new explainer is initiated

  -> data              : 95512 rows 17 cols
  -> target variable   : Parameter 'y' was a pandas.Series. Converted to a numpy.ndarray.
  -> target variable   : 95512 values
  -> model_class       : sklearn.ensemble._forest.RandomForestClassifier (default)
  -> label             : RF classifier with OH encoding
  -> predict function  : <function yhat_proba_default at 0x7f9efd2dab70> will be used (default)
  -> predict function  : Accepts only pandas.DataFrame, numpy.ndarray causes problems.
  -> predicted values  : min = 0.0, mean = 0.371, max = 1.0
  -> model type        : classification will be used (default)
  -> residual function : difference between y and yhat (default)
  -> residuals         : min = -0.943, mean = -0.00182, max = 0.957
  -> model_info        : package sklearn

A new explainer has been created!

1. Predykcja modelu dla wybranej obserwacji ze zbioru danych

Weźmy pod uwagę 2300-tną obserwację w zbiorze treningowym. Spójrzmy, jak wyglądają wartości poszczególnych cech.

In [7]:
X_train.iloc[2300]
Out[7]:
lead_time                                 82
arrival_date_week_number                  22
stays_in_weekend_nights                    0
stays_in_week_nights                       3
adults                                     2
previous_cancellations                     0
previous_bookings_not_canceled             0
required_car_parking_spaces                0
total_of_special_requests                  2
adr                                    189.0
booking_changes                            0
hotel                             City Hotel
market_segment                     Online TA
country                                  NLD
reserved_room_type                         D
customer_type                      Transient
agent                                    9.0
Name: 69469, dtype: object
In [8]:
print(f"""Wyliczona predykcja dla wybranej obserwacji w zbiorze treningowym to: {predict_train[2300]},
          \nPrawopodobieństwo przyporządkowania do targetu 1: {predict_proba_train[2300][1]}, 
          \nPrawidłowa klasyfikacja to: {y_train.iloc[2300]}.""")
Wyliczona predykcja dla wybranej obserwacji w zbiorze treningowym to: 1,
          
Prawopodobieństwo przyporządkowania do targetu 1: 0.65625, 
          
Prawidłowa klasyfikacja to: 1.

Dla wybranej obserwacji model przewiduje target = 1, co oznacza, że rezerwacja zostanie odwołana. Jest to rzeczywiście odpowiednia wartość - rezerwacja ta jest oznaczona w zbiorze jako odwołana.

2. Wyliczenie dekompozycji predykcji modelu dla wcześniej wybranej obserwacji używając profili Ceteris Paribus

In [9]:
cp1 = explainer.predict_profile(X_train.iloc[2300,:])
Calculating ceteris paribus: 100%|██████████| 17/17 [00:02<00:00,  8.17it/s]
In [10]:
cp1.plot(title = "Profile CP dla zmiennych numerycznych") 
In [11]:
cp1.plot(variable_type = "categorical", title = "Profile CP dla zmiennych kategorycznych") 
  • Zauważmy, że niemalże każda zmiana wartości pojedynczej zmiennej (zarówno numerycznej, jak i kategorycznej) skutkowałaby zmianą predykcji w stronę labelu 0, a więc nieodwołania rezerwacji.
  • W wielu przypadkach oznaczałoby to również zmianę ostatecznej klasyfikacji, gdyż w przypadu tej obserwacji predykcja jest blisko granicy decyzyjnej, jak np. gdyby rezerwacja dotyczyła dwóch nocy w dni robocze więcej lub dotyczyłaby trzech, a nie dwóch dorosłych.
  • Skutek odwrotny, a więc wpływ na predykcję w stronę odwołania rezerwacji miałby fakt wcześniejszych odwołań, co wydaje się naturalne. Przy czym widzimy, że nie liczy się tu sama liczba odwołań, a fakt zaistnienia przynajmniej jednej takiej sytuacji.
  • Zupełnie bez znaczenia dla predykcji byłyby ewentualne zmiany w ilości wcześniej nieodwołanych rezerwacji klienta.

3. Obserwacje o różnych profilach CP dla tej samej zmiennej

Oprócz otrzymanej już wyżej dekompozycji, weźmiemy pod uwagę dekompozycję 4242-tnej obserwacji ze zbioru treningowego. Skupimy się na zmiennych numerycznych.

In [12]:
X_train.iloc[4242]
Out[12]:
lead_time                                   36
arrival_date_week_number                    45
stays_in_weekend_nights                      0
stays_in_week_nights                         2
adults                                       2
previous_cancellations                       0
previous_bookings_not_canceled               0
required_car_parking_spaces                  0
total_of_special_requests                    2
adr                                       55.8
booking_changes                              0
hotel                             Resort Hotel
market_segment                       Online TA
country                                    PRT
reserved_room_type                           E
customer_type                        Transient
agent                                    240.0
Name: 29809, dtype: object
In [13]:
print(f"""Wyliczona predykcja dla wybranej obserwacji w zbiorze treningowym to: {predict_train[4242]},
          \nPrawopodobieństwo przyporządkowania do targetu 1: {predict_proba_train[4242][1]}, 
          \nPrawidłowa klasyfikacja to: {y_train.iloc[4242]}.""")
Wyliczona predykcja dla wybranej obserwacji w zbiorze treningowym to: 0,
          
Prawopodobieństwo przyporządkowania do targetu 1: 0.225, 
          
Prawidłowa klasyfikacja to: 0.
In [14]:
cp2 = explainer.predict_profile(X_train.iloc[4242,:])
Calculating ceteris paribus: 100%|██████████| 17/17 [00:02<00:00,  7.96it/s]
In [15]:
cp2.plot(title = "Profile CP dla zmiennych numerycznych") 

Możemy wskazać wiele cech, których profile znacząco się różnią dla zdekomponowanych obserwacji.

Dla przejrzystości przedstawmy je obok siebie. W lewej kolumnie znajdują się wykresy odpowiadające profilom dla pierwszej z analizowanych obserwacji (2300.), po prawej - dla drugiej (4242.).

  • Liczba dni poprzedzających termin rezerwacji lead_time:

    • dla obserwacji po lewej wraz ze wzrostem wartości zmiennej odpowiedź modelu waha się, ale jest mniejsza i nie osiąga już pierwotnego poziomu,
    • dla obserwacji po prawej wraz ze wzrostem wartości zmiennej odpowiedź modelu rośnie, jest większa niż pierworny poziom.
  • Liczba dni roboczych, które obejmuje rezerwacja stays_in_week_nights:

    • dla obserwacji po lewej wraz ze wzrostem wartości zmiennej odpowiedź modelu maleje,
    • dla obserwacji po prawej wraz ze wzrostem wartości zmiennej odpowiedź modelu rośnie.
  • Liczba wcześniejszych nieodwołanych rezerwacji klienta previous_bookings_not_canceled (obie obserwacje mają wartość 0):

    • dla obserwacji po lewej wraz ze wzrostem wartości zmiennej odpowiedź modelu nie zmienia się,
    • dla obserwacji po prawej wraz ze wzrostem wartości zmiennej odpowiedź modelu początkowo maleje, po czym stabilizuje się.
  • Liczba zmian dotyczących rezerwacji booking_changes (obie obserwacje mają wartość 0):

    • dla obserwacji po lewej wraz ze wzrostem wartości zmiennej odpowiedź modelu maleje, po czym nieznacznie rośnie i stabilizuje się
    • dla obserwacji po prawej wraz ze wzrostem wartości zmiennej odpowiedź modelu zachowuje się odwrotnie - rośnie, po czym nieznacznie maleje i stabilizuje się.

W szczególności uwagę należy zwrócić na dwa ostatnie punkty, gdzie profile różnią się mimo takich samych wartości zmiennych. Pokazuje to interakcje w analizowanym modelu.

Różnice są duże natomiast należy zauwazyć, że obserwacje są z innych klas. Dlatego przygotuję jeszcze dekompozycje drugiej obserwacji zaklasyfikowanej jako 0 - rezerwacja nieodwołana.

In [16]:
X_train.iloc[1,:]
Out[16]:
lead_time                                 34
arrival_date_week_number                  28
stays_in_weekend_nights                    1
stays_in_week_nights                       1
adults                                     2
previous_cancellations                     0
previous_bookings_not_canceled             0
required_car_parking_spaces                0
total_of_special_requests                  0
adr                                    146.0
booking_changes                            0
hotel                             City Hotel
market_segment                        Direct
country                                  FRA
reserved_room_type                         A
customer_type                      Transient
agent                                   14.0
Name: 115851, dtype: object
In [17]:
print(f"""Wyliczona predykcja dla wybranej obserwacji w zbiorze treningowym to: {predict_train[1]},
          \nPrawopodobieństwo przyporządkowania do targetu 1: {predict_proba_train[1][1]}, 
          \nPrawidłowa klasyfikacja to: {y_train.iloc[1]}.""")
Wyliczona predykcja dla wybranej obserwacji w zbiorze treningowym to: 0,
          
Prawopodobieństwo przyporządkowania do targetu 1: 0.10625, 
          
Prawidłowa klasyfikacja to: 0.
In [18]:
cp3 = explainer.predict_profile(X_train.iloc[1,:])
Calculating ceteris paribus: 100%|██████████| 17/17 [00:02<00:00,  8.06it/s]
In [19]:
cp3.plot(title = "Profile CP dla zmiennych numerycznych")

Również porównując obserwacje z tej samej klasy możemy znaleźć różnice w poszczególnych profilach.

Dla przejrzystości przedstawmy je znów obok siebie. W lewej kolumnie znajdują się wykresy odpowiadające profilom dla obserwacji 1. zdekomponowanej bezpośrednio powyżej, po prawej dla obserwacji 4242. zdekompowanej wcześniej.

  • Liczba zmian dotyczących rezerwacji booking_changes (obie obserwacje mają wartość 0):

    • dla obserwacji po lewej wraz ze wzrostem wartości zmiennej odpowiedź modelu maleje, po czym nieznacznie rośnie i stabilizuje się,
    • dla obserwacji po prawej wraz ze wzrostem wartości zmiennej odpowiedź modelu zachowuje się odwrotnie - rośnie, po czym nieznacznie maleje i stabilizuje się.
  • Liczba wcześniejszych nieodwołanych rezerwacji klienta previous_bookings_not_canceled (obie obserwacje mają wartość 0):

    • dla obserwacji po lewej wraz ze wzrostem wartości zmiennej odpowiedź modelu nieznacznie rośnie i stabilizuje się,
    • dla obserwacji po prawej wraz ze wzrostem wartości zmiennej odpowiedź modelu początkowo gwałtownie maleje, po czym stabilizuje się.
  • Numer tygodnia roku, którego dotyczy rezerwacja arrival_date_week_number:

    • dla obserwacji po lewej odpowiedź modelu zachowuje się nieregularnie, a pierwotna wartość obserwacji odpowiada minimalnej wartości predykcji,
    • dla obserwacji po prawej wraz ze wzrostem wartości zmiennej powyżej 10 odpowiedź modelu maleje, ale nie ma minium w okolicy wartości 28.

Podsumowanie

  • Metoda profili Ceteris Paribus jest intuicyjna dzięki swojej reprezentacji graficznej, ale jeśli zmiennych jest dużo przeglądanie i porównywanie może być uciążliwe.
  • Metoda ta umożliwia głębszy wgląd na wpływ zmiennych na predykcję modelu niż poprzednio wykorzystywane metody wyjaśnień lokalnych. Nie określa ilościowo znaczenia poszczególnych, a ocenia wpływ wybranej zmiennej objaśniającej na zmiany predykcji modelu wywołane zmianami wartości tej zmiennej.
  • Pozwala wyciągać wnioski dotyczące wrażliwości modelu.
  • Pozwala znaleźć zmienne, których wartości nie mają wpływu na predykcję modelu przy ustalonych wartościach pozostałych zmiennych.
In [ ]: